#!/usr/bin/perl
use strict;
use warnings;

#use Data::Dumper;

#	standard modules
use DateTime;
use DateTime::Format::ISO8601;
use DateTime::Format::MySQL;
use DBI;
use File::Path;
use Getopt::Long;
use Module::Load::Conditional qw[can_load];
use URI::Escape;

#	myth module
use MythTV;

#	constants
use constant {
	WEEKDAY => 0,
	WEEKEND => 1,
	HOLIDAY => 3,

	TITLE		=> 0x01,
	SUBTITLE	=> 0x02,
	DESCRIPTION	=> 0x04,
	CHANNEL		=> 0x08

	};

#	settings
my $settings=
{
	guideoffset=>	6,

	# channels for which to display guide data
	# a list of channel names, use an empty array no channels should be shown
	# or undef if all should be shown
	guidechannels=>	
		[
			"BBC ONE",
			"BBC TWO",
			"ITV1",
			"Channel 4",
			"Channel 5",
			"BBC FOUR",
			"More 4",
			"BBC Radio 4",
		],

	# channels for which to display movies
	# a list of channel names, use an empty array no channels should be shown
	# or undef if all should be shown
	moviechannels =>undef,
#	moviechannels =>
#		[
#			"BBC ONE",
#			"BBC TWO",
#			"ITV1",
#			"Channel 4",
#			"Channel 5",
#			"BBC FOUR",
#			"More 4",
#			"BBC Radio 4",
#		],

	pickschannels =>undef,
#	pickschannels =>
#		[
#			"BBC TWO",
#		],

	zones =>
		{
			morning		=>	[06,11],
			afternoon	=>	[12,17],
			evening		=>	[18,20],
			night		=>	[21,03],
			deadzone	=>	[04,05],
		},

	days =>
		{
			&WEEKDAY		=>
			{
					morning		=> TITLE,
					afternoon	=> TITLE,
					evening		=> TITLE|SUBTITLE|DESCRIPTION,
					night		=> TITLE|SUBTITLE|DESCRIPTION,
					deadzone	=> 0
			},

			&WEEKEND		=>
			{
					morning		=> TITLE,
					afternoon	=> TITLE|SUBTITLE|DESCRIPTION,
					evening		=> TITLE|SUBTITLE|DESCRIPTION,
					night		=> TITLE|SUBTITLE|DESCRIPTION,
					deadzone	=> 0
			},

			&HOLIDAY		=>
			{
					morning		=> TITLE|SUBTITLE|DESCRIPTION,
					afternoon	=> TITLE|SUBTITLE|DESCRIPTION,
					evening		=> TITLE|SUBTITLE|DESCRIPTION,
					night		=> TITLE|SUBTITLE|DESCRIPTION,
					deadzone	=> 0
			},

		},

	categories	=>
		{
			"Kids"		=>	0,
			"Sports"	=>	TITLE
		}
		
};

#	local timezone
my $tz=DateTime::TimeZone->new( name => 'local' );

#	options
my $options = 
	{
		start=>		DateTime->today(time_zone =>$tz),
		days=>		1,
		picks=>		0,
	};

GetOptions ($options, 
	'days=i',
	'picks'
	);


my $outdir="$ENV{'HOME'}/.mythtv-guide/temp";
#File::Path::remove_tree($outdir);
File::Path::mkpath($outdir);
my $myth = new MythTV();


#	bayes globals
my $bayes;
my $stopwords;

#	load bayes specific modules
if ($options->{picks})
{
	if (!can_load(
		modules=>
		{
			'Algorithm::NaiveBayes' => undef,
			'Lingua::StopWords'		=> undef,
		})) 
	{
		warn("Can't enable picks without Algorithm::NaiveBayes and Lingua::StopWords, disabling.");
		$options->{picks}=0;
	}
	else
	{
		$stopwords = Lingua::StopWords::getStopWords('en');
	}

}

my $dbh = $myth->{'dbh'};

if ($options->{picks})
{
	$bayes = Algorithm::NaiveBayes->new;
}

#	fetch all scheduled recordings
my $scheduled=FetchUpcoming();

#	flag which channels we're interested in
my %channels;

foreach my $channel  (@{$settings->{'guidechannels'}}) 
{
	$channels{$channel}->{'guide'}=1;
}

if (defined($settings->{'moviechannels'}))
{
	foreach my $channel  (@{$settings->{'moviechannels'}}) 
	{
		$channels{$channel}->{'movies'}=1;
	}
}

if ($options->{picks})
{
	if (defined($settings->{'pickschannels'}))
	{
		foreach my $channel  (@{$settings->{'pickschannels'}}) 
		{
			$channels{$channel}->{'picks'}=1;
		}
	}
}

my $results=FetchProgramGuide();

if ($options->{picks})
{
	$bayes->train();
}

ExportProgramGuide($results);

sub formatHTMLHead
{
	my $title=shift;
	return qq|
		<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
		<html xmlns="http://www.w3.org/1999/xhtml">
		<head>
		<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
		<title>$title</title>
		<link rel="stylesheet" type="text/css" href="Guide.css" />
		</head><body>
	|;
}

# ===========================================================================
# ExportProgramGuide
# ===========================================================================

sub ExportProgramGuide
{
	my $data=shift;

	my $title='TV Guide';

	my $dt=DateTime->today();

	open(XHTMLGUIDE,sprintf ">$outdir/Guide.html",$dt->strftime('%F')) 
		or die "Can't open file".sprintf ">$outdir/Guide %s.html",$dt->strftime('%F');

	print(XHTMLGUIDE formatHTMLHead($title));

	print(XHTMLGUIDE qq|<h1>$title</h1>|);

	print(XHTMLGUIDE qq|<p>|);
	foreach my $day (sort(keys(%{$data->{'guide'}})))
	{
		my $dt=DateTime::Format::ISO8601->parse_datetime($day);
	
		printf(XHTMLGUIDE qq|<a href='%s'>%s</a><br/>\n|,uri_escape("Guide $day.html"),$dt->strftime('%A %e %B %Y'));
		ExportProgramGuideDay($day,$data);
	}
	print(XHTMLGUIDE qq|</p>|);

	print(XHTMLGUIDE qq|
	</body>
	</html>
	|);

	close(XHTMLGUIDE);
}

# ===========================================================================
# ExportProgramGuideDay
# ===========================================================================

sub ExportProgramGuideDay
{	
	my $day=shift;
	my $data=shift;

	my $dt=DateTime::Format::ISO8601->parse_datetime($day);

	my $title=$dt->strftime('%A %e %B %Y');

	open(XHTMLDAY,sprintf ">$outdir/Guide %s.html",$dt->strftime('%F')) 
		or die "Can't open file".sprintf ">$outdir/Guide %s.html",$dt->strftime('%F');

	print(XHTMLDAY formatHTMLHead($title));

	print(XHTMLDAY qq|<h1>$title</h1>|);

	print(XHTMLDAY qq|
		<a href='Movies $day.html'>Movies</a><br/>\n|);

	ExportProgramGuideMovies($day,$data);

	if ($options->{picks})
	{
		print(XHTMLDAY qq|
			<a href='Picks $day.html'>Picks</a><br/>\n|);

		ExportProgramGuidePicks($day,$data);
	}

	foreach my $channel (@{$settings->{guidechannels}}) 
	{
		print(XHTMLDAY qq|
			<a href='Guide $day $channel.html'>$channel</a><br/>\n|);
		ExportProgramGuideChannel($day,$channel,$data);
	}

	print(XHTMLDAY qq|
	</body>
	</html>
	|);

	close(XHTMLDAY);
}

# ===========================================================================
# ExportProgramGuidePicks
# ===========================================================================

sub ExportProgramGuidePicks
{
	my $day=shift;
	my $data=shift;

	my $dt=DateTime::Format::ISO8601->parse_datetime($day);

	my $title=sprintf("Picks %s",$dt->strftime('%A %e %B %Y'));

	open(XHTMLPICKS,">$outdir/Picks $day.html") 
		or die;

	print(XHTMLPICKS formatHTMLHead($title));

	print(XHTMLPICKS qq|<h1>$title</h1>|);

	my $pickskeys={};
	my @picks;

	foreach my $channel (keys(%{$data->{'all'}->{$day}})) 
	{
		foreach my $program (@{$data->{'all'}->{$day}->{$channel}}) 
		{
			if (!exists($pickskeys->{$program->{'title'}}->{$program->{'subtitle'}})) 
			{
				my $predict=PredictBayes($program->{title},$program->{subtitle},$program->{description});
				if (	!IsScheduled($program)
					&&	!$program->{currentlyrecorded}		
					&&	!$program->{previouslyrecorded}		
					&&	$predict>0
					) 
				{
					#printf("Ham:%s %s\n",$program->{channel},$program->{title});

					$program->{'hammyness'}=$predict;
					push(@picks,$program);
					$pickskeys->{$program->{'title'}}->{$program->{'subtitle'}}=1;
				}
			}
		}
	}

	my $cpicks=0;
	foreach my $program (sort(
			{
				$a->{'hammyness'}-$b->{'hammyness'}
			}
			(@picks)))
	{
		if ($cpicks<10) 
		{
			my $programhtml=FormatProgram($program,TITLE|SUBTITLE|DESCRIPTION|CHANNEL);
			print(XHTMLPICKS $programhtml);
		}
		$cpicks++;
	}

	print(XHTMLPICKS qq|
	</body>
	</html>
	|);
	

	close(XHTMLPICKS);
}

# ===========================================================================
# ExportProgramGuideMovies
# ===========================================================================

sub ExportProgramGuideMovies
{
	my $day=shift;
	my $data=shift;

	my $dt=DateTime::Format::ISO8601->parse_datetime($day);

	my $title=sprintf("Movies %s",$dt->strftime('%A %e %B %Y'));

	open(XHTMLMOVIES,">$outdir/Movies $day.html") 
		or die;

	print(XHTMLMOVIES formatHTMLHead($title));

	print(XHTMLMOVIES qq|<h1>$title</h1>|);

	my $moviekeys={};
	my @movies;

	foreach my $channel (keys(%{$data->{'movies'}->{$day}})) 
	{
		foreach my $program (@{$data->{'movies'}->{$day}->{$channel}}) 
		{
			if (!exists($moviekeys->{$program->{'title'}}->{$program->{'subtitle'}})) 
			{
				push(@movies,$program);
				$moviekeys->{$program->{'title'}}->{$program->{'subtitle'}}=1;
			}
		}
	}

	foreach my $program (sort(
			{
				$a->{'starttime'}->epoch-$b->{'starttime'}->epoch
			}
			(@movies)))
	{
		my $programhtml=FormatProgram($program,TITLE|SUBTITLE|DESCRIPTION|CHANNEL);
		print(XHTMLMOVIES $programhtml);
	}


	print(XHTMLMOVIES qq|
	</body>
	</html>
	|);
	

	close(XHTMLMOVIES);

}

# ===========================================================================
# ExportProgramGuideChannel
# ===========================================================================

sub ExportProgramGuideChannel
{
	my $day=shift;
	my $channel=shift;
	my $data=shift;

	my $dt=DateTime::Format::ISO8601->parse_datetime($day);
	my $daytype=GetDayType($dt);

	my $title=sprintf("%s %s",$channel,$dt->strftime('%A %e %B %Y'));


	open(XHTMLCHANNEL,sprintf ">$outdir/Guide %s $channel.html",$dt->strftime('%F')) 
		or die;

	print(XHTMLCHANNEL formatHTMLHead($title));

	print(XHTMLCHANNEL qq|<h1>$title</h1>|);

	foreach my $program (@{$data->{'guide'}->{$day}->{$channel}}) 
	{
		#if (	$program->{'starttime'}>DateTime->now()) 
		{
			my $zone=GetZone($program->{'starttime'});
			my $programhtml;

			if (defined($zone)) 
			{
				#	categories take precedence
				if (defined($settings->{'categories'}->{$program->{category}})) 
				{
					$programhtml=FormatProgram($program,$settings->{'categories'}->{$program->{category}});
				}
				else
				{
					#printf	"%s %s %s\n",$program->{'starttime'},$daytype,$zone;
					if (defined($settings->{'days'}->{$daytype}->{$zone})) 
					{
						$programhtml=FormatProgram(
							$program,$settings->{'days'}->{$daytype}->{$zone});
					}
					else
					{
						if (!defined($settings->{'days'}->{$daytype})) 
						{
							warn("No zones for $daytype");
						}
						else
						{
							warn("No flags for $daytype $zone");
						}
					}
				}
				
				if ($programhtml) 
				{
					print(XHTMLCHANNEL $programhtml);
				}
			}
			else
			{
				warn("No zone for program starting at $program->{'starttime'}");
			}
		}
	}

	print(XHTMLCHANNEL qq|
	</body>
	</html>
	|);
	

	close(XHTMLCHANNEL);
}

# ===========================================================================
# GetDayType
# ===========================================================================

sub GetDayType
{
	my $dt=shift;

	my $dow = $dt->day_of_week;

	if ($dow>=0 && $dow<=4) 
	{
		return WEEKDAY;
	}
	else
	{
		return WEEKEND;
	}
}

# ===========================================================================
# GetZone
# ===========================================================================

sub GetZone
{
	my $dt=shift;

	foreach my $zone (keys(%{$settings->{zones}}))
	{
		#printf ">>%s %s %s\n",$dt->hour,$settings->{zones}->{$zone}->[0],$settings->{zones}->{$zone}->[1];

		my $zonedata=$settings->{zones}->{$zone};

		if ($zonedata->[0]<$zonedata->[1]) 
		{
			if (	$dt->hour>=$settings->{zones}->{$zone}->[0]
				&&	$dt->hour<=$settings->{zones}->{$zone}->[1]) 
			{
				return $zone;
			}
		}
		else
		{
			if ((		$dt->hour>=$settings->{zones}->{$zone}->[0]
					&&	$dt->hour<=23)
				||
				(		$dt->hour<=$settings->{zones}->{$zone}->[1]
					&&	$dt->hour>=0
				))
			{
				return $zone;
			}
		}
	}
	return undef;
}

# ===========================================================================
# FormatProgram
# ===========================================================================

sub FormatProgram
{
	my $program=shift;
	my $flags=shift;
	my $result;

	if ($flags!=0) 
	{
		my $class='';

		if (IsScheduled($program)) 
		{
			$class=qq|class ='scheduled'|;
		}
		elsif ($program->{'currentlyrecorded'}) 
		{
			$class=qq|class ='currentlyrecorded'|;
		}
		elsif ($program->{'previouslyrecorded'}) 
		{
			$class=qq|class ='previouslyrecorded'|;
		}

		$result.=qq|<p $class>\n|;

		if ($flags & TITLE) 
		{
				$result.=sprintf(qq|<b>%02d:%02d %s</b>. %s\n|,
						$program->{'starttime'}->hour,
						$program->{'starttime'}->minute,
						$program->{'title'},
						($flags & CHANNEL)?'('.$program->{'channel'}.')':'');

		}
		if ($flags & SUBTITLE) 
		{
				if ($program->{'subtitle'} ne '') 
				{
					$result.=sprintf(qq|<i>%s</i>. \n|,
							$program->{'subtitle'});
				}
		}
		if ($flags & DESCRIPTION) 
		{
				$result.=sprintf(qq|%s\n|,
					$program->{'description'});
		}

		# extra info
		my @info;

		if ($program->{'category'}) 
		{
			push(@info,qq|$program->{'category'}|);
		}

		if ($program->{'airdate'}!=0) 
		{
			push(@info,qq|<span class='airdate'>$program->{'airdate'}</span>|);
		}

		if (	$program->{'episode'}
			&&	$program->{'episode_of'}) 
		{
			push(@info,
				sprintf(qq|<span class='episode'>%s/%s</span>|,
					$program->{'episode'},
					$program->{'episode_of'}
				));
		}

		if (IsScheduled($program)) 
		{
			push(@info,qq|<span class='schedulestatus'>Will Record</span>|);
		}
		elsif ($program->{'currentlyrecorded'}) 
		{
			push(@info,qq|<span class='schedulestatus'>Currently Recorded</span>|);
		}
		elsif ($program->{'previouslyrecorded'}) 
		{
			push(@info,qq|<span class='schedulestatus'>Previously Recorded</span>|);
		}



		if (scalar(@info))
		{
			$result.=' ('.join(' ',@info).')';
		}

		if (	IsScheduled($program)
			&&	!$program->{'dupin'}==6
			) 
		{
			$result.=qq|<b>[Will Record]</b>|;
		}
		
		$result.=qq|</p>\n|;
	}
	else
	{	
		$result=qq|<!-- Skipped $program->{starttime}: $program->{title} $program->{subtitle} ($program->{category}) -->\n|;
	}
}

# ===========================================================================
# IsScheduled
# ===========================================================================

sub IsScheduled
{
	my $program=shift;

	foreach my $scheduledProgram (@{$scheduled}) 
	{
		if (	$scheduledProgram->{'chanid'}== $program->{'chanid'}
			&&	$scheduledProgram->{'starttime'}== $program->{'unix_start'})
		{
			return 1;
		}
	}
	return 0;
}

# ===========================================================================
# GetWords
# ===========================================================================

sub GetWords
{
	my $text=shift;
	my $weight=shift;
	my $words=shift;

	foreach my $word (split(/\W+/,$text)) 
	{
		if (!exists($stopwords->{$word})) 
		{
			$words->{$word}+=$weight;
		}
	}

}

# ===========================================================================
# TrainBayes
# ===========================================================================

sub TrainBayes
{
	my $title=shift;
	my $subtitle=shift;
	my $description=shift;

	my $label=shift;
	my $words={};

	GetWords($title,1,$words);
	GetWords($subtitle,1,$words);
	GetWords($description,1,$words);

	$bayes->add_instance(attributes=>$words,label=>$label);
}

# ===========================================================================
# PredictBayes
# ===========================================================================

sub PredictBayes
{
	my $title=shift;
	my $subtitle=shift;
	my $description=shift;
	
	my $words={};

	GetWords($title,1,$words);
	GetWords($subtitle,1,$words);
	GetWords($description,1,$words);

	my $result=$bayes->predict(attributes=>$words);

	if (	defined($result->{'Ham'})
		&&	defined($result->{'Spam'}))
	{
		return $result->{'Ham'}-$result->{'Spam'};
	}
	elsif (defined($result->{'Ham'}))
	{
		return 1;
	}
	else
	{
		return 0;
	}


	
}

# ===========================================================================
# FetchUpcoming
# Fetch scheduled recordings from the backend, return an arry of programs
# ===========================================================================

sub FetchUpcoming
{
	my $result=[];
	my %data=$myth->backend_rows('QUERY_GETALLPENDING',2);

	foreach my $row (@{$data{'rows'}}) 
	{
		#print "@{$row}\n";

		my $program=$myth->new_program(@{$row});

		push(@{$result},$program);

		if ($options->{picks})
		{
			TrainBayes(
				$program->{'title'},
				$program->{'subtitle'},
				$program->{'description'},
				'Ham'
			);
		}
	}

	return $result;
}

# ===========================================================================
# FetchProgramGuide
# Fetch program guide data from the database
# ===========================================================================
sub FetchProgramGuide
{
	my $start_day=shift;
	my $ham=0;
	my $spam=0;


	my $results={};

	$results->{'guide'}={};
	$results->{'movies'}={};

	# Have to get the guide data from the database
	# Myth Protocol/Guide - MythTV Official Wiki - http://www.mythtv.org/wiki/Myth_Protocol/Guide
	# "Unfortunately, there is no known Myth Command to fetch all guide data given a criteria."

	my @where;
	my %selectChannels;

	if (	defined($settings->{guidechannels})
		&&	defined($settings->{moviechannels})
		&&	(
					defined($settings->{pickschannels})
				||	!$options->{picks}
			)
		)
	{
		foreach my $channel (@{$settings->{guidechannels}}) 
		{
			$selectChannels{"'$channel'"}++;
		}

		if (defined($settings->{'moviechannels'}))
		{
			foreach my $channel (@{$settings->{moviechannels}}) 
			{
				$selectChannels{"'$channel'"}++;
			}
		}

		foreach my $channel (@{$settings->{pickschannels}}) 
		{
			$selectChannels{"'$channel'"}++;
		}

		if (scalar(keys(%selectChannels))>=0) 
		{
			push(@where,sprintf("C.Name in (%s)",join(',',keys(%selectChannels))));
		}
		else
		{
			die "Nothing to do!";
		}
	}

	my $start;
	my $end;

	if (!$options->{picks})
	{
		$start=$options->{start}->add(hours=>$settings->{guideoffset});
		$end=$start->clone()->add(hours=>24*$options->{days});
		push(@where,"P.starttime>=? AND P.endtime<?");
	}

	my $where='';
	if (scalar(@where)) 
	{
		$where="WHERE ".join(' AND ',@where);
	}


	my $sql=qq|
		SELECT DISTINCT
			C.name as channel,C.chanid,
			P.starttime,P.endtime,P.title,P.subtitle,P.description,P.category,P.airdate,P.partnumber,P.parttotal,UNIX_TIMESTAMP(P.starttime) as unix_start,
			R.title as currentlyrecorded,
			O.title as previouslyrecord
		FROM `mythconverg`.`program` as P 
		LEFT JOIN mythconverg.channel as C ON (P.chanid=C.chanid) 
		LEFT JOIN mythconverg.recordedprogram as R ON (P.title=R.title AND P.subtitle=R.subtitle)
		LEFT JOIN mythconverg.oldrecorded as O ON (P.title=O.title AND P.subtitle=O.subtitle)
		$where
		ORDER BY starttime
		|;

	print "$sql\n";

	my $sth = $dbh->prepare($sql)
		or die("!!");

	if (	!defined($start)
		||	!defined($end))
	{
		$sth->execute() 
			or die("!!");
	}
	else
	{
		printf("Start: %s; End: %s\n",$start,$end);
		$sth->execute(
			DateTime::Format::MySQL->format_datetime($start),
			DateTime::Format::MySQL->format_datetime($end)
			)
			or die("!!");
	}

	while (my $row= $sth->fetchrow_hashref())
	{
		#printf "%s\t%s\t%s\t(%s)\t%s\n",$row->{starttime},$row->{channel},$row->{title},$row->{subtitle},$row->{description};

		my $program={
				'starttime'		=>	DateTime::Format::MySQL->parse_datetime($row->{starttime}),
				'channel'		=>	$row->{channel},
				'title'			=>	$row->{title},
				'subtitle'		=>	$row->{subtitle},
				'description'	=>	$row->{description},
				'category'		=>  $row->{category},
				'currentlyrecorded'		
								=>  defined($row->{'currentlyrecorded'}),
				'previouslyrecorded'		
								=>  defined($row->{'previouslyrecorded'}),
				'chanid'		=>	$row->{chanid},
				'unix_start'	=>	$row->{unix_start},
				'airdate'		=>	$row->{airdate},
				'episode'		=>	$row->{partnumber},
				'episode_of'	=>	$row->{parttotal},
			};


		my $day=$program->{'starttime'}->clone();

		$day->subtract( hours => $settings->{'guideoffset'} );


		if ($program->{'starttime'}>DateTime->now(time_zone =>$tz)) 
		{
			# program is in the future, include in data

			if (exists($channels{$row->{channel}}->{'guide'}))
			{
				push(@{$results->{'guide'}->{$day->strftime('%F')}->{$row->{'channel'}}},$program);
			}
			
			if ((	!defined($settings->{moviechannels})
				 ||	exists($channels{$row->{channel}}->{'movies'})
				)
				&&	$program->{'category'} eq "Movie"
				)
			{
				push(@{$results->{'movies'}->{$day->strftime('%F')}->{$row->{'channel'}}},$program);
			}
			
			push(@{$results->{'all'}->{$day->strftime('%F')}->{$row->{'channel'}}},$program);
		}
		else
		{
			# program is in the past, train the bayes classifier with the data
			#print "Past: $program->{'title'}\n";
			if	(	$options->{picks}
				 &&	(	!defined($settings->{pickschannels})
					 ||	exists($channels{$row->{channel}}->{'picks'}))
				)
			{
				if (	$program->{'currentlyrecorded'}
					||	$program->{'previouslyrecorded'})
				{
					TrainBayes(
						$program->{'title'},
						$program->{'subtitle'},
						$program->{'description'},
						'Ham'
						);
					$ham++;
				}
				else
				{
					TrainBayes(
						$program->{'title'},
						$program->{'subtitle'},
						$program->{'description'},
						'Spam'
						);
					$spam++;
				}
			}
		}

	}

	#printf("Ham: %d; Spam %d\n",$ham,$spam);

	$sth->finish();
	
	return $results;
}




